package edu.fiu.cis.selfhealingdisk;

import java.util.HashMap;
import java.util.Iterator;

public class SHMemory {

	private final long MAX_TIME_IN_BUFFER;
	private final long timeSteps;
	private final HashMap<Long, MemoryEntry> shMem;
	private final int MAX_MEM;
	
	private final int cleanThr;
	private int wcount;

	//local stats
	private int maxMem;
	private int hits;
	private int cleans;
	
	public SHMemory(int memorySize, int clt, long maxMemTime) {
		super();
		
		this.shMem = new HashMap<Long, MemoryEntry>();
		this.MAX_MEM = memorySize;
		this.cleanThr = clt;
		;
		this.MAX_TIME_IN_BUFFER = maxMemTime;
		this.timeSteps = (long) (this.MAX_TIME_IN_BUFFER/1E5);
		maxMem = 0;
		wcount = 0;
		hits = 0;
		cleans = 0;
	}

	/**
	 * Return if the memory completely clear. This is there are not pending
	 * events. This function is used to check if we can finish the application
	 * 
	 * @return true, if the memory is empty or there all the frames are clear
	 */
	public boolean isMemoryClear() {
		if (this.memorySize() == 0)
			return true;
		else {
			for (MemoryEntry e : this.shMem.values()) {
				if (!e.isClear())
					return false;
			}
		}
		return true;
	}

	/**
	 * Return the number of used blocks in memory at this moment
	 * 
	 * @return
	 */
	public int memorySize() {
		int sz = 0;
		for (MemoryEntry e : this.shMem.values())
			sz += e.memEntrySize();
		return sz;
	}

	/**
	 * A simple algorithm to clean the memory used for buffering. This cleaner
	 * in this version is synchronous, this is should be called before assign a
	 * new memory location. The call returns all request that should be cleaned
	 * in the particular period of time
	 * 
	 * @param time
	 *            the time for the modification
	 * @return
	 */
	/*
	 * public void cleanMemory(long time,IORequestList requests) { long t =
	 * time; MemoryEntry me = null;
	 * 
	 * // Clean all the entries that are older that MAX_TIME_IN_BUFFER int
	 * cleaned = 0; for (Iterator<MemoryEntry> it =
	 * this.shMem.values().iterator(); it.hasNext() && cleaned < this.cleanThr
	 * ;) { me = it.next(); if (!me.isLoaded() || me.isWriting()) { continue; }
	 * if (me.isClear()) { it.remove(); cleaned++; } else if (time -
	 * me.getLastAccess() > MAX_TIME_IN_BUFFER) { IORequestList r =
	 * me.getRequests(t); requests.addAll(r); t += r.size(); cleaned++; } }
	 * 
	 * 
	 * }
	 */

	public void cleanMemory(long time, IORequestList requests) {
		long t = time;
		MemoryEntry me = null;
		wcount++;
		
		if (this.shMem.size() > maxMem) maxMem = this.shMem.size();
		if (wcount == this.timeSteps) {
			// Clean all the entries that are older that MAX_TIME_IN_BUFFER

			for (Iterator<MemoryEntry> it = this.shMem.values().iterator(); it
					.hasNext();) {
				me = it.next();
				if (!me.isLoaded() || me.isWriting()) {
					continue;
				}
				if (me.isClear() && this.shMem.size() >= MAX_MEM) {
					it.remove();
				} else if (time - me.getLastAccess() > MAX_TIME_IN_BUFFER) {
					IORequestList r = me.getRequests(t);
					requests.addAll(r);
					t += r.size();

				}
			}
			wcount = 0;
			cleans++;
		}
	}

	/**
	 * Reserve the memory for a read event. The space is reserved but the
	 * content of the block is still wait to be loaded from disk. The simulation
	 * of the memory allocation for the block is implemented with the isLoaded
	 * event. A modification is implemented with the setDirty
	 * 
	 * @param pba
	 * @param size
	 * @param time
	 * @returns true if the memory is reserved, false the page is memory
	 */
	public boolean addEntry(long pba, int size, long time) {
		if (this.shMem.containsKey(pba)) {
			hits++;
			return false;
		}	
		this.shMem.put(pba, new MemoryEntry(pba, size, time));
		return true;
	}

	/**
	 * Simulated the end of the read event for this PB. This event should be
	 * called when the READ operation on this particular PB is finished.
	 * 
	 * @param pba
	 *            the physical block for the parity group
	 * @param time
	 *            the time for the modification
	 * @throws Exception
	 *             there is not a previous block associated with this parity
	 *             group
	 */
	public void readParityComplete(long pba, long time) {
		MemoryEntry e = (this.shMem.get(pba));
		e.setLoaded();
		e.setLastAccess(time);
	}

	/**
	 * Set the dirty bit for a particular parity block. This event should be
	 * called when system writes a change in the PB
	 * 
	 * @param pba
	 *            the physical block for the parity group
	 * @param time
	 *            the time for the modification
	 * @throws Exception
	 *             there is not a previous block associated with this parity
	 *             group or the read event is not finished
	 */
	public void writeParityChange(long pba, long time) {
		MemoryEntry e = (this.shMem.get(pba));
		e.setDirty();
	}

	/**
	 * Clear the dirty bit for a particular parity block. This event should be
	 * called when the pending parity write for the particular pba is finished.
	 * 
	 * @param pba
	 *            the physical block for the parity group
	 * @param time
	 *            the time for the modification
	 * @throws Exception
	 *             there is not a previous block associated with this parity
	 *             group or the read event is not finished
	 */
	public void clearParityChange(long pba, long time) {
		MemoryEntry e = (this.shMem.get(pba));
		e.clearDirty();
		e.setWriting(false);
		e.setLastAccess(time);
	}

	/**
	 * Blocks a write request if there is any parity event pending in the
	 * associated parity group This function check for the write safeness
	 * 
	 * @param pba
	 *            the physical block for the parity group
	 * @param ior
	 *            the request that is tested for blocking
	 * @return true if the request is added to the pending events queue, false
	 *         if there is not reason to block the event
	 * @throws Exception
	 */
	public boolean blockRequest(long pba, IORequest ior) {
		MemoryEntry e = null;
		if ((e = this.shMem.get(pba)) == null || (e.isClear()))
			return false;
		e.addRequest(ior);
		return true;
	}

	/**
	 * Check if there is a pending event for the parity block pba
	 * 
	 * @param pba
	 * @return
	 */
	public boolean pendingEvents(long pba) {
		return this.shMem.get(pba) == null && this.shMem.get(pba).isClear();
	}

	/**
	 * Flush all the request in memory
	 * 
	 * @param timestamp
	 * @return
	 */
	public IORequestList flushMemory(long timestamp) {
		IORequestList requests = new IORequestList();
		for (MemoryEntry e : this.shMem.values())
			if (e.isDirty()) {
				requests.addAll(e.getRequests(timestamp++));
			}
		return requests;
	}

	/**
	 * Flush all the request in memory for a particular PB
	 * 
	 * @param timestamp
	 * @return
	 * @throws Exception
	 */
	public IORequestList flushMemory(long pba, long timestamp) throws Exception {
		MemoryEntry e = null;
		if ((e = this.shMem.get(pba)) == null || !e.isLoaded())
			throw new Exception("[SHMem] There is not page in memory for" + pba);
		IORequestList requests = new IORequestList();
		if (e.isDirty()) {
			requests.addAll(e.getRequests(timestamp++));
		}
		return requests;
	}

	public String toString() {
		return this.shMem.values().toString();
	}

	public int getCleanThr() {
		return cleanThr;
	}
	
	public int getMemoryHits() {
		return this.hits;
	}

	public int cleanOperationsCount() {
		return this.cleans;
	}
	
	public int getMaxMem() {
		return this.maxMem;
	}

}
